import { Direction, SortOrder } from './sort-order';
import { emptyPageData, PageData } from './page-data';
import { SortDirection } from '@angular/material/sort';

export const MAX_SAFE_PAGE_SIZE = 2147483647;

export type PageLinkSearchFunction<T> = (entity: T, textSearch: string, searchProperty?: string) => boolean;

export interface PageQueryParam extends Partial<SortOrder> {
  textSearch?: string;
  pageSize?: number;
  page?: number;
  action?: string;
}

export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction<any> {
  return (entity, textSearch) => defaultPageLinkSearch(entity, textSearch, searchProperty);
}

function defaultPageLinkSearch<T>(
  entity: T,
  textSearch: string,
  searchProperty?: string
): boolean {
  if (textSearch === null || !textSearch.length) {
    return true;
  }
  const expected = ('' + textSearch).toLowerCase();
  if (searchProperty && searchProperty.length) {
    if (Object.prototype.hasOwnProperty.call(entity, searchProperty)) {
      const val = (entity as Record<string, unknown>)[searchProperty];
      if (val !== null) {
        if (val !== Object(val)) {
          const actual = ('' + val).toLowerCase();
          if (actual.indexOf(expected) !== -1) {
            return true;
          }
        }
      }
    }
  } else {
    const entityRecord = entity as Record<string, unknown>;
    for (const key of Object.keys(entityRecord)) {
      const val = entityRecord[key];
      if (val !== null) {
        if (val !== Object(val)) {
          const actual = ('' + val).toLowerCase();
          if (actual.indexOf(expected) !== -1) {
            return true;
          }
        } else if (isObject(val)) {
          if (defaultPageLinkSearch(val, textSearch)) {
            return true;
          }
        }
      }
    }
  }
  return false;
}

function isObject(value: unknown): boolean {
  return value !== null && typeof value === 'object';
}

function getDescendantProp(obj: unknown, path: string): unknown {
  if (obj && typeof obj === 'object' && 'hasOwnProperty' in obj && (obj as Record<string, unknown>).hasOwnProperty(path)) {
    return (obj as Record<string, unknown>)[path];
  }
  return path.split('.').reduce((acc, part) => {
    if (acc && typeof acc === 'object' && part in acc) {
      return (acc as Record<string, unknown>)[part];
    }
    return undefined;
  }, obj as unknown) as unknown;
}

export function sortItems(item1: unknown, item2: unknown, property: string, asc: boolean): number {
  const item1Value = getDescendantProp(item1, property);
  const item2Value = getDescendantProp(item2, property);
  let result = 0;
  if (item1Value !== item2Value) {
    const item1Type = typeof item1Value;
    const item2Type = typeof item2Value;
    if (item1Type === 'number' && item2Type === 'number') {
      result = (item1Value as number) - (item2Value as number);
    } else if (item1Type === 'string' && item2Type === 'string') {
      result = (item1Value as string).localeCompare(item2Value as string);
    } else if ((item1Type === 'boolean' && item2Type === 'boolean') || (item1Type !== item2Type)) {
      if (item1Value && !item2Value) {
        result = 1;
      } else if (!item1Value && item2Value) {
        result = -1;
      }
    }
  }
  return asc ? result : result * -1;
}

export class PageLink {
  textSearch: string | null;
  pageSize: number;
  page: number;
  sortOrder: SortOrder | null;

  constructor(pageSize: number, page: number = 0, textSearch: string | null = null, sortOrder: SortOrder | null = null) {
    this.textSearch = textSearch;
    this.pageSize = pageSize;
    this.page = page;
    this.sortOrder = sortOrder;
  }

  public nextPageLink(): PageLink {
    return new PageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder);
  }

  public toQuery(): string {
    let query = `?pageSize=${this.pageSize}&page=${this.page}`;
    const textSearchParams = this.textSearch?.trim();
    if (textSearchParams?.length) {
      const textSearch = encodeURIComponent(textSearchParams);
      query += `&textSearch=${textSearch}`;
    }
    if (this.sortOrder) {
      query += `&sortProperty=${this.sortOrder.property}&sortOrder=${this.sortOrder.direction}`;
    }
    return query;
  }

  public sort(item1: unknown, item2: unknown): number {
    if (this.sortOrder) {
      const sortProperty = this.sortOrder.property;
      const asc = this.sortOrder.direction === Direction.ASC;
      return sortItems(item1, item2, sortProperty, asc);
    }
    return 0;
  }

  public filterData<T>(
    data: Array<T>,
    searchFunction: PageLinkSearchFunction<T> = defaultPageLinkSearchFunction()
  ): PageData<T> {
    const pageData = emptyPageData<T>();
    pageData.data = [...data];
    if (this.textSearch && this.textSearch.length) {
      pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch!));
    }
    pageData.totalElements = pageData.data.length;
    pageData.totalPages = this.pageSize === Number.POSITIVE_INFINITY ? 1 : Math.ceil(pageData.totalElements / this.pageSize);
    if (this.sortOrder) {
      const sortProperty = this.sortOrder.property;
      const asc = this.sortOrder.direction === Direction.ASC;
      pageData.data = pageData.data.sort((a, b) => sortItems(a, b, sortProperty, asc));
    }
    if (this.pageSize !== Number.POSITIVE_INFINITY) {
      const startIndex = this.pageSize * this.page;
      pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
      pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
    }
    return pageData;
  }

  public sortDirection(): SortDirection {
    if (this.sortOrder) {
      return (this.sortOrder.direction + '').toLowerCase() as SortDirection;
    } else {
      return '' as SortDirection;
    }
  }
}

export class TimePageLink extends PageLink {
  startTime: number | null;
  endTime: number | null;

  constructor(
    pageSize: number,
    page: number = 0,
    textSearch: string | null = null,
    sortOrder: SortOrder | null = null,
    startTime: number | null = null,
    endTime: number | null = null
  ) {
    super(pageSize, page, textSearch, sortOrder);
    this.startTime = startTime;
    this.endTime = endTime;
  }

  public override nextPageLink(): TimePageLink {
    return new TimePageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder, this.startTime, this.endTime);
  }

  public override toQuery(): string {
    let query = super.toQuery();
    if (this.startTime) {
      query += `&startTime=${this.startTime}`;
    }
    if (this.endTime) {
      query += `&endTime=${this.endTime}`;
    }
    return query;
  }
}

